﻿using Avalonia.Controls.Mixins;
using Avalonia.Data;
using Avalonia.Layout;
using GraphShape.Controls.Animations;
using GraphShape.Controls.Extensions;
// ReSharper disable IdentifierTypo
// ReSharper disable CommentTypo

namespace GraphShape.Controls
{
    /// <summary>
    /// Graph canvas.
    /// </summary>
    public class GraphCanvas : Panel
    {
        static GraphCanvas()
        {
            AffectsRender<GraphCanvas>(XProperty, YProperty, OrigoProperty);
            AffectsArrange<GraphCanvas>(XProperty, YProperty, OrigoProperty);
            AffectsMeasure<GraphCanvas>(XProperty, YProperty, OrigoProperty);
            AffectsParentArrange<GraphCanvas>(XProperty, YProperty, OrigoProperty);
            AffectsParentMeasure<GraphCanvas>(XProperty, YProperty, OrigoProperty);

            XProperty.Changed.Subscribe(X_PropertyChanged);
            YProperty.Changed.Subscribe(Y_PropertyChanged);
        }

        #region Attached Dependency Property registrations

        /// <summary>
        /// X attached dependency property.
        /// </summary>
        [NotNull]
        public static readonly AttachedProperty<double> XProperty
            = AvaloniaProperty.RegisterAttached<GraphCanvas, Interactive, double>(
                                                                                  "X",
                                                                                  double.NaN,
                                                                                  false,
                                                                                  BindingMode.TwoWay);

        private static void X_PropertyChanged(AvaloniaPropertyChangedEventArgs<double> args)
        {
            var xChange = args.GetNewValue<double>() - args.GetOldValue<double>();
            PositionChanged(args.Sender, xChange, 0);
        }

        /// <summary>
        /// Y attached dependency property.
        /// </summary>
        [NotNull]
        public static readonly AttachedProperty<double> YProperty
            = AvaloniaProperty.RegisterAttached<GraphCanvas, Interactive, double>(
                                                                                  "Y",
                                                                                  double.NaN,
                                                                                  false,
                                                                                  BindingMode.TwoWay);

        private static void Y_PropertyChanged(AvaloniaPropertyChangedEventArgs<double> args)
        {
            var yChange = args.GetNewValue<double>() - args.GetOldValue<double>();
            PositionChanged(args.Sender, 0, yChange);
        }

        private static void PositionChanged(AvaloniaObject d, double xChange, double yChange)
        {
            if (d is Control uiElement)
            {
                uiElement.RaiseEvent(
                                     new PositionChangedEventArgs(PositionChangedEvent, uiElement, xChange, yChange));
            }
        }

        #endregion

        #region Attached Properties

        /// <summary>
        /// Gets the X attached property value.
        /// </summary>
        public static double GetX(AvaloniaObject obj)
        {
            return obj.GetValue(XProperty);
        }

        /// <summary>
        /// Sets the X attached property value.
        /// </summary>
        public static void SetX(AvaloniaObject obj, double value)
        {
            obj.SetValue(XProperty, value);
        }

        /// <summary>
        /// Gets the Y attached property value.
        /// </summary>
        public static double GetY(AvaloniaObject obj)
        {
            return obj.GetValue(YProperty);
        }

        /// <summary>
        /// Sets the Y attached property value.
        /// </summary>
        public static void SetY(AvaloniaObject obj, double value)
        {
            obj.SetValue(YProperty, value);
        }

        #endregion

        #region Attached Routed Events

        /// <summary>
        /// Position changed event.
        /// </summary>
        [NotNull]
        public static readonly RoutedEvent<PositionChangedEventArgs> PositionChangedEvent
            = RoutedEvent.Register<GraphCanvas, PositionChangedEventArgs>(
                                                                          "PositionChanged",
                                                                          RoutingStrategies.Bubble);

        /// <summary>
        /// Adds a new <see cref="PositionChangedEvent"/> handler.
        /// </summary>
        public static void AddPositionChangedHandler(AvaloniaObject d, EventHandler<PositionChangedEventArgs> handler)
        {
            if (d is Control uiElement)
            {
                uiElement.AddHandler(PositionChangedEvent, handler);
            }
        }

        /// <summary>
        /// Removes the given <paramref name="handler"/> from <see cref="PositionChangedEvent"/>.
        /// </summary>
        public static void RemovePositionChangedHandler(AvaloniaObject d, EventHandler<PositionChangedEventArgs> handler)
        {
            if (d is Control uiElement)
            {
                uiElement.RemoveHandler(PositionChangedEvent, handler);
            }
        }

        #endregion

        #region Animation length

        /// <summary>
        /// Gets or sets the length of the animation.
        /// If the length of the animations is 0:0:0.000, there won't be any animations.
        /// </summary>
        public TimeSpan AnimationLength
        {
            get => GetValue(AnimationLengthProperty);
            set => SetValue(AnimationLengthProperty, value);
        }

        /// <summary>
        /// Animation length dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<TimeSpan> AnimationLengthProperty
            = AvaloniaProperty.Register<GraphCanvas, TimeSpan>(
                                                               nameof(AnimationLength),
                                                               new TimeSpan(0, 0, 0, 0, 500));

        #endregion

        #region CreationAnimation

        /// <summary>
        /// Gets or sets the animation controller for the 'Control Creation' animation.
        /// </summary>
        public ITransition CreationTransition
        {
            get => GetValue(CreationTransitionProperty);
            set => SetValue(CreationTransitionProperty, value);
        }

        /// <summary>
        /// Transition creation dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<ITransition> CreationTransitionProperty
            = AvaloniaProperty.Register<GraphCanvas, ITransition>(
                                                                  nameof(CreationTransition),
                                                                  new FadeInTransition());

        #endregion

        #region IsAnimationEnabled

        /// <summary>
        /// If this property is true, and the other animation disabler
        /// properties are also true, the animation is enabled.
        /// If this is false, the animations will be disabled.
        /// </summary>
        public bool IsAnimationEnabled
        {
            get => GetValue(IsAnimationEnabledProperty);
            set => SetValue(IsAnimationEnabledProperty, value);
        }

        /// <summary>
        /// Animation enabled dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<bool> IsAnimationEnabledProperty
            = AvaloniaProperty.Register<GraphCanvas, bool>(
                                                           nameof(IsAnimationEnabled),
                                                           true);

        #endregion

        #region MoveAnimation

        /// <summary>
        /// Gets or sets the animation controller for the 'Control Moving' animation.
        /// </summary>
        public IAnimation MoveAnimation
        {
            get => GetValue(MoveAnimationProperty);
            set => SetValue(MoveAnimationProperty, value);
        }

        /// <summary>
        /// Animation movement dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<IAnimation> MoveAnimationProperty
            = AvaloniaProperty.Register<GraphCanvas, IAnimation>(
                                                                 nameof(MoveAnimation),
                                                                 new SimpleMoveAnimation());

        #endregion

        #region DestructionAnimation

        /// <summary>
        /// Gets or sets the transition controller for the 'Control Destruction' animation.
        /// </summary>
        public ITransition DestructionTransition
        {
            get => GetValue(DestructionTransitionProperty);
            set => SetValue(DestructionTransitionProperty, value);
        }

        /// <summary>
        /// Transition destruction dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<ITransition> DestructionTransitionProperty
            = AvaloniaProperty.Register<GraphCanvas, ITransition>(
                                                                  nameof(DestructionTransition),
                                                                  new FadeOutTransition());

        #endregion

        #region Origo

        /// <summary>
        /// Gets or sets the virtual origo of the canvas.
        /// </summary>
        public gPoint Origo
        {
            get => GetValue(OrigoProperty);
            set => SetValue(OrigoProperty, value);
        }

        /// <summary>
        /// Origo dependency property.
        /// </summary>
        [NotNull]
        public static readonly StyledProperty<gPoint> OrigoProperty
            = AvaloniaProperty.Register<GraphCanvas, gPoint>(nameof(Origo));

        #endregion

        #region Translation

        /// <summary>
        /// Translation.
        /// </summary>
        public aVector Translation
        {
            get => GetValue(TranslationProperty);
            protected set => SetValue(TranslationProperty, value);
        }

        /// <summary>
        /// Translation property key.
        /// </summary>
        [NotNull]
        protected static readonly AttachedProperty<aVector> TranslationProperty
            = AvaloniaProperty.RegisterAttached<GraphCanvas, Interactive, aVector>(nameof(Translation));

        #endregion

        #region Measure & Arrange

        /// <summary>
        /// The position of the topLeft corner of the most top-left vertex.
        /// </summary>
        private aPoint _topLeft;

        /// <summary>
        /// The position of the bottom right corner of the most  bottom-right vertex.
        /// </summary>
        private aPoint _bottomRight;

        /// <summary>
        /// Arranges the size of the control.
        /// </summary>
        /// <param name="finalSize">The arranged size of the control.</param>
        /// <returns>The size of the control.</returns>
        protected override aSize ArrangeOverride(aSize finalSize)
        {
            var translate = new aVector(-_topLeft.X, -_topLeft.Y);
            var graphSize = _bottomRight - _topLeft;

            if (double.IsNaN(graphSize.X)
                || double.IsNaN(graphSize.Y)
                || double.IsInfinity(graphSize.X)
                || double.IsInfinity(graphSize.Y))
            {
                translate = new aVector(0, 0);
            }

            Translation = translate;
            double w;
            double h;

            if (Children.Count > 0)
            {
                w = double.NegativeInfinity;
                h = double.NegativeInfinity;
            }
            else
            {
                w = 0;
                h = 0;
            }

            // Translate with the topLeft
            foreach (Control child in Children)
            {
                var x = GetX(child);
                var y = GetY(child);
                if (double.IsNaN(x) || double.IsNaN(y))
                {
                    // Not a vertex, set the coordinates of the top-left corner
                    x = double.IsNaN(x) ? translate.X : x;
                    y = double.IsNaN(y) ? translate.Y : y;
                }
                else
                {
                    // This is a vertex
                    x += translate.X;
                    y += translate.Y;

                    // Get the top-left corner
                    x -= child.DesiredSize.Width  * 0.5;
                    y -= child.DesiredSize.Height * 0.5;
                }
                child.Arrange(
                              new aRect(
                                        new aPoint(x, y),
                                        child.DesiredSize));

                w = Math.Max(0, Math.Max(x, x + child.DesiredSize.Width));
                h = Math.Max(0, Math.Max(y, y + child.DesiredSize.Height));
            }

            return new aSize(w, h);
        }

        /// <summary>
        /// Overridden measure. It calculates a size where all of 
        /// of the vertices are visible.
        /// </summary>
        /// <param name="availableSize">The size constraint.</param>
        /// <returns>The calculated size.</returns>
        protected override aSize MeasureOverride(aSize availableSize)
        {
            _topLeft     = new aPoint(double.PositiveInfinity, double.PositiveInfinity);
            _bottomRight = new aPoint(double.NegativeInfinity, double.NegativeInfinity);

            foreach (var child in Children)
            {
                // Measure the child
                child.Measure(availableSize);

                // Get the position of the vertex
                var left = GetX(child);
                var top  = GetY(child);

                var halfWidth  = child.DesiredSize.Width  * 0.5;
                var halfHeight = child.DesiredSize.Height * 0.5;

                if (double.IsNaN(left) || double.IsNaN(top))
                {
                    left = halfWidth;
                    top  = halfHeight;
                }

                // Get the top left corner point
                var    x = Math.Min(_topLeft.X, left - halfWidth  - Origo.X);
                var y = Math.Min(_topLeft.Y, top  - halfHeight - Origo.Y);
                _topLeft = new aPoint(x, y);


                // Calculate the bottom right corner point
                x            = Math.Max(_bottomRight.X, left + halfWidth  - Origo.X);
                y            = Math.Max(_bottomRight.Y, top  + halfHeight - Origo.Y);
                _bottomRight = new aPoint(x, y);
            }

            var graphPoint = (_bottomRight - _topLeft);
            var graphSize  = new aSize(Math.Max(0, graphPoint.X), Math.Max(0, graphPoint.Y));

            if (double.IsNaN(graphSize.Width)
                || double.IsNaN(graphSize.Height)
                || double.IsInfinity(graphSize.Width)
                || double.IsInfinity(graphSize.Height))
            {
                return default(aSize);
            }

            return graphSize;
        }

        #endregion

        /// <summary>
        /// The layout process will be initialized with the current vertices positions.
        /// </summary>
        public virtual void ContinueLayout()
        {
        }

        /// <summary>
        /// The layout process will be started without initial vertices positions.
        /// </summary>
        public virtual void Relayout()
        {
        }

        [CanBeNull]
        private IAnimationContext _animationContext;

        /// <summary>
        /// Gets the context of the animation.
        /// </summary>
        public virtual IAnimationContext AnimationContext => _animationContext ??= new AnimationContext(this);

        /// <summary>
        /// Gets whether the animation could be run, or not.
        /// </summary>
        public virtual bool CanAnimate => IsAnimationEnabled;

        /// <summary>
        /// Does a transition for the <paramref name="control"/> which has been
        /// already added to this container.
        /// </summary>
        /// <param name="control">The control which has been added.</param>
        protected virtual void RunCreationTransition([NotNull] Control control)
        {
            if (CanAnimate)
            {
                CreationTransition?.Run(AnimationContext, control, AnimationLength);
            }
        }

        /// <summary>
        /// Animates the position of the given <paramref name="control"/> to the given positions.
        /// </summary>
        /// <param name="control">The control which should be moved.</param>
        /// <param name="x">The new horizontal position of the control.</param>
        /// <param name="y">The new vertical position of the control.</param>
        protected virtual void RunMoveAnimation([NotNull] Control control, double x, double y)
        {
            if (MoveAnimation != null && CanAnimate)
            {
                // Animate to the given position
                MoveAnimation.Animate(AnimationContext, control, x, y, AnimationLength);
            }
            else
            {
                // Cannot animate, only the given positions
                SetX(control, x);
                SetY(control, y);
            }
        }

        /// <summary>
        /// Transitions a control which gonna' be removed from this container.
        /// </summary>
        /// <param name="control">The control which will be removed.</param>
        /// <param name="dontRemoveAfter">
        /// If it's true, the control won't be removed automatically
        /// from this container's <see cref="P:System.Windows.Controls.Panel.Children"/>.
        /// </param>
        protected virtual void RunDestructionTransition([NotNull] Control control, bool dontRemoveAfter)
        {
            if (DestructionTransition is null || !CanAnimate)
            {
                if (!dontRemoveAfter)
                {
                    Children.Remove(control);
                }
            }
            else
            {
                if (dontRemoveAfter)
                {
                    DestructionTransition.Run(AnimationContext, control, AnimationLength);
                }
                else
                {
                    DestructionTransition.Run(AnimationContext, control, AnimationLength, x => Children.Remove(x));
                }
            }
        }
    }
}